/*      $Id: date.c 1077 2010-04-27 02:59:51Z phrakt $    */
/*
 * Copyright (c) 2003 Jean-Francois Brousseau <phrakt@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/time.h>

#include <time.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <canopy/log.h>
#include <canopy/string.h>

#include "http.h"
#include "date.h"
#include "private.h"


static const char * const weekdays[] = {
	"Sunday",
	"Monday",
	"Tuesday",
	"Wednesday",
	"Thursday",
	"Friday",
	"Saturday"
};


static const char * const months[] = {
	"January",
	"February",
	"March",
	"April",
	"May",
	"June",
	"July",
	"August",
	"September",
	"October",
	"November",
	"December"
};




static int  http_date_parse_rfc1123    (char *, struct tm *);
static int  http_date_parse_rfc850     (char *, struct tm *);
static int  http_date_parse_ansic      (char *, struct tm *);
static int  http_date_parse_iso8601    (char *, struct tm *);
static int  http_date_parse_time       (char *, struct tm *);
static int  http_date_guessfmt         (const char *, enum http_date_format *);
static int  http_date_getmonthbyname   (const char *);
static int  http_date_getwdaybyname    (const char *);



/*
 * The <http_date_default> variable specifies the format to use when
 * HTTP_DATE_DEFAULT is used.  It can obviously not be set to
 * HTTP_DATE_DEFAULT or HTTP_DATE_GUESS.
 */
static enum http_date_format http_date_default = HTTP_DATE_RFC1123;


#if 0
static struct http_date_fmt {
	char     df_name[16];

	int    (*df_parser)(char *, struct tm *);
	int    (*df_formatter)(time_t, char *, size_t);
} date_formats[] = {
	{ "RFC1123",    http_date_parse_rfc1123,    NULL, },
	{ "RFC850",     http_date_parse_rfc850,     NULL, },
	{ "ISO8601",    http_date_parse_iso8601,    NULL, },
};
#endif




/*
 * http_date_setdefault()
 *
 * Sets the library's default date format to <format>.
 *
 * Returns 0 on success, or -1 if the format is invalid as a default.
 */
int
http_date_setdefault(enum http_date_format format)
{
	if ((format == HTTP_DATE_DEFAULT) || (format == HTTP_DATE_GUESS)) {
		return (-1);
	}

	http_date_default = format;

	return (0);
}

/*
 * http_date_format()
 *
 * Format the date represented in seconds since the Epoch in <date> to a
 * string representation.  The exact format is specified in <fmt> and can be
 * any of the following formats:
 *
 *	-RFC 1123 (preferred):	Sun, 06 Nov 1994 08:49:37 GMT
 *	-RFC 850 (obsolete):	Sunday, 06-Nov-94 08:49:37 GMT
 *	-ANSI C ctime():	Sun Nov  6 08:49:37 1994
 *	-ISO 8601:		1994-11-06T08:49:37Z
 *
 * Returns the length of the string that the function tried to copy, or -1
 * if an error other than truncation occurred.  If the return value is equal
 * to or larger than <dlen>, the buffer <dst> was too small and the name
 * was truncated.
 */
ssize_t
http_date_format(time_t date, enum http_date_format fmt, char *dst, size_t dlen)
{
	char wdbuf[4], monbuf[4];
	size_t len;
	int ret = -1;
	struct tm res;

	gmtime_r(&date, &res);

	if (fmt == HTTP_DATE_DEFAULT)
		fmt = http_date_default;

	switch (fmt) {
	case HTTP_DATE_RFC1123:
		strlcpy(wdbuf, weekdays[res.tm_wday], sizeof(wdbuf));
		strlcpy(monbuf, months[res.tm_mon], sizeof(monbuf));
		ret = snprintf(dst, dlen, "%s, %02u %s %4u %02u:%02u:%02u GMT",
		    wdbuf, res.tm_mday, monbuf, res.tm_year + 1900,
		    res.tm_hour, res.tm_min, res.tm_sec);
		break;

	case HTTP_DATE_RFC850:
		strlcpy(monbuf, months[res.tm_mon], sizeof(monbuf));
		ret = snprintf(dst, dlen, "%s, %02u-%s-%u %02u:%02u:%02u GMT",
		    weekdays[res.tm_wday], res.tm_mday, monbuf,
		    res.tm_year, res.tm_hour, res.tm_min, res.tm_sec);
		break;

	case HTTP_DATE_ANSIC:
		if (dlen < 26) {
			http_log_err("buffer too small for ANSI C time format");
			return ((ssize_t) -1);
		}

		if (ctime_r(&date, dst) == NULL) {
			http_log_errno("failed to format time");
			return ((ssize_t) -1);
		}

		len = strlen(dst);
		if ((len > 0) && (dst[len - 1] == '\n'))
			dst[--len] = '\0';

		ret = (int)len;

		break;

	case HTTP_DATE_ISO8601:
		ret = snprintf(dst, dlen, "%4d-%02d-%02dT%02u:%02u:%02uZ",
		    res.tm_year + 1900, res.tm_mon + 1, res.tm_mday,
		    res.tm_hour, res.tm_min, res.tm_sec);
		break;

	default:
		http_log_err("invalid date format %u", fmt);
		break;
	}

	return ((ssize_t) ret);
}

/*
 * http_date_parse()
 *
 * Returns 0 on success, or -1 on failure.
 */
int
http_date_parse(const char *date, enum http_date_format fmt, time_t *result)
{
	int ret = 0;
	char buf[64];
	struct tm res;

	if ((fmt == HTTP_DATE_GUESS) &&
	    (http_date_guessfmt(date, &fmt) == -1)) {
		http_log_err("failed to guess date format");
		return (-1);
	}

	memset(&res, 0, sizeof(res));
	strlcpy(buf, date, sizeof(buf));

	switch (fmt) {
	case HTTP_DATE_RFC1123:
		ret = http_date_parse_rfc1123(buf, &res);
		break;

	case HTTP_DATE_RFC850:
		ret = http_date_parse_rfc850(buf, &res);
		break;

	case HTTP_DATE_ANSIC:
		ret = http_date_parse_ansic(buf, &res);

	case HTTP_DATE_ISO8601:
		ret = http_date_parse_iso8601(buf, &res);
		break;

	default:
		http_log_err("no support for date format in parser");
		ret = -1;
	}

	http_log_debug("date string: `%s'", date);
	http_log_debug("tm_year=%d, tm_mon=%d, tm_mday=%d, "
	    "tm_hour=%d, tm_min=%d, tm_sec=%d",
	    res.tm_year, res.tm_mon, res.tm_mday, res.tm_hour, res.tm_min,
	    res.tm_sec);


	*result = mktime(&res);

	return (ret);
}

/*
 *
 */
static int
http_date_parse_rfc1123(char *date, struct tm *res)
{
	/* Ex: Sun, 06 Nov 1994 08:49:37 GMT */
	date[3] = date[7] = date[11] = date[16] = date[25] = '\0';

	res->tm_wday = http_date_getwdaybyname(date);
	res->tm_mday = atoi(&date[5]);
	res->tm_mon = http_date_getmonthbyname(&date[8]);
	res->tm_year = atoi(&date[12]) - 1900;

	http_date_parse_time(&date[17], res);

	return (0);
}

/*
 *
 *	-RFC 850 (obsolete):	Sunday, 06-Nov-94 08:49:37 GMT
 */
static int
http_date_parse_rfc850(char *date, struct tm *res)
{
	char *ep, *sp;

	sp = date;

	if ((ep = strchr(sp, ',')) == NULL)
		return (-1);

	*(ep++) = '\0';
	res->tm_wday = http_date_getwdaybyname(sp);

	sp = ep;
	if (*(sp++) != ' ') {
		return (-1);
	}

	if ((ep = strchr(sp, ' ')) == NULL) {
		return (-1);
	}

	*(ep++) = '\0';

	if (strlen(sp) != 9) {
		return (-1);
	}

	sp[2] = sp[6] = '\0';
	res->tm_mday = atoi(sp);
	res->tm_mon = http_date_getmonthbyname(&sp[3]);
	res->tm_year = atoi(&sp[7]);

	sp = ep;
	if ((ep = strchr(sp, ' ')) == NULL) {
		return (-1);
	}

	*(ep++) = '\0';

	if (http_date_parse_time(sp, res) == -1)
		return (-1);

	return (0);
}

/*
 *
 */
static int
http_date_parse_ansic(char *date, struct tm *res)
{
	return (-1);
}

/*
 *
 */
static int
http_date_parse_iso8601(char *date, struct tm *res)
{
	date[4] = date[7] = date[10] = date[19] = '\0';

	res->tm_year = atoi(date) - 1900;
	res->tm_mon = atoi(&date[5]) - 1;
	res->tm_mday = atoi(&date[8]);

	if (http_date_parse_time(&date[11], res) == -1)
		return (-1);

	return (0);
}

/*
 * http_date_parse_time()
 *
 * Parse the time-of-day portion of a date string according to the pattern
 * 'hh:mm:ss' where 'hh' are the hours of the day (0-23), 'mm' are the minutes
 * of the hour (0-59), and 'ss' are the seconds (0-59).
 *
 * Returns 0 on success, or -1 on failure.
 */
static int
http_date_parse_time(char *timestr, struct tm *res)
{
	if (strlen(timestr) != 8) {
		http_log_err(
		    "invalid string length for time specification");
		return (-1);
	}

	timestr[2] = timestr[5] = timestr[8] = '\0';

	res->tm_hour = atoi(timestr);
	if ((res->tm_hour < 0) || (res->tm_hour > 23)) {
		http_log_err("invalid value for hour of day");
		return (-1);
	}

	res->tm_min = atoi(&timestr[3]);
	if ((res->tm_min < 0) || (res->tm_min > 59)) {
		http_log_err("invalid value for minutes");
		return (-1);
	}

	res->tm_sec = atoi(&timestr[6]);
	if ((res->tm_sec < 0) || (res->tm_sec > 59)) {
		http_log_err("invalid value for seconds");
		return (-1);
	}

	return (0);
}

/*
 * http_date_guessfmt()
 *
 * Attempts to guess the format of the date string specified in <datestr>
 * with a very simplistic heuristic and stores the result in <fmt> if it
 * found a match.
 *
 * Returns 0 on success, or -1 on failure.
 *
 * This function is relatively naive and will probably guess wrong if you
 * pass it a date string of a format unknown to the library.
 */
static int
http_date_guessfmt(const char *datestr, enum http_date_format *fmt)
{
	const char *sp;

	if ((sp = strchr(datestr, ',')) == NULL) {
		if ((sp = strchr(datestr, ' ')) == NULL)
			*fmt = HTTP_DATE_ISO8601;
		else
			*fmt = HTTP_DATE_ANSIC;
	}
	else {
		if ((sp - datestr) == 3)
			*fmt = HTTP_DATE_RFC1123;
		else
			*fmt = HTTP_DATE_RFC850;
	}

	return (0);
}

/*
 * http_date_getwdaybyname()
 *
 */
static int
http_date_getwdaybyname(const char *name)
{
	int i;
	size_t len;

	len = strlen(name);

	for (i = 0; i < 12; i++)
		if (strncmp(weekdays[i], name, len) == 0)
			return (i);

	return (-1);
}

/*
 * http_date_getmonthbyname()
 *
 */
static int
http_date_getmonthbyname(const char *name)
{
	int i;
	size_t len;

	len = strlen(name);

	for (i = 0; i < 12; i++)
		if (strncmp(months[i], name, len) == 0)
			return (i);

	return (-1);
}
